package it.tukano.xml;

import it.tukano.io.FileExt;
import it.tukano.io.FileExt.OutputStreamCall;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * XML manipulation utilities
 * @author pgi
 */
public class XmlDoc {

    public static XmlDoc newInstance(Reader reader) {
        return new XmlDoc(reader);
    }

    public static XmlDoc newInstanceFromXml(String meta) {
        return XmlDoc.newInstance(new StringReader(meta));
    }

    public void store(FileExt docFile) {
        final TransformerFactory transformer = TransformerFactory.newInstance();
        final DOMSource source = new DOMSource(getDocument());
        docFile.withOutputStream(new OutputStreamCall<Void>() {

            public Void call(OutputStream out) throws Throwable {
                Transformer t = transformer.newTransformer();
                t.transform(source, new StreamResult(out));
                return null;
            }
        });
    }

    /**
     * The entity of a xml document
     */
    public static interface XmlNode {

        public <T extends Enum<T>> T getEnum(Object name, Class<T> type, T defaultValue);

        /**
         * Add a child to this node
         * @param name the name of the child to add
         * @return this node
         */
        public XmlNode addChild(Object name);

        /** Scan the branch looking for the first node with the given tag*/
        public XmlNode findFirstChild(Object tag);
        
        /**
         * Set a generic attribute for this node. Uses String.valueOf(value)
         * @param name the name of the attribute to set
         * @param value the value to set
         * @return this node
         */
        public XmlNode setAttribute(Object name, Object value);

        /**
         * Sets an array of attributes. The array is a name-value pairs list
         * @param nameValuePairs the list of name-value pairs
         * @return this
         */
        public XmlNode setAttributes(Object... nameValuePairs);

        /**
         * Set a decimal attribute, formatted to up to decimalDigits
         * @param name the name of the attribute to set
         * @param value the decimal value to use
         * @param decimalDigits how many digits after the zero to use
         * @return this
         */
        public XmlNode setAttribute(Object name, Number value, int decimalDigits);

        /**
         * Returns the string attribute with the given name
         * @param name the name of the attribute.
         * @return the value or null
         */
        public String getString(Object name);
        
        public java.util.Properties getAttributesAsProperties();

        /**
         * Returns the string attribute with the given name or a default value
         * @param name the name of the attribute to get
         * @param defaultValue the default value to return
         * @return the value of the attribut or the default value
         */
        public String getString(Object name, String defaultValue);

        /**
         * Returns the double attribute with the given name
         * @param name the name of the attribute
         * @return the value or null
         */
        public Double getDouble(Object name);

        /**
         * Returns the integer attribute with the given name
         * @param name the name of the attribute
         * @return the value or null
         */
        public Integer getInteger(Object name);

        /**
         * Returns the first child whose name is the given one
         * @param name the name of the child to get
         * @return the requested child or null
         */
        public XmlNode getFirstChild(Object name);

        /**
         * Lists all the chidren of this node with the given name
         * @param name the name of the children to get
         * @return the list of children, can be empty
         */
        public Iterable<XmlNode> listChildren(Object name);

        /**
         * Returns the content of this node as a string
         * @return the content of this node as a string
         */
        public String getContentAsString();

        /**
         * Returns the tag of this node
         * @return the tag of this node
         */
        public String getTag();

        /**
         * List the children of this node with the given name
         * @param name the name of the children
         * @param scanChildren true to traverse the entire branch
         * @return  the list of nodes witht the given name
         */
        public Iterable<XmlNode> listChildren(Object name, boolean scanChildren);

        /**
         * List the children of this node
         * @return the children of this node
         */
        public List<XmlNode> listChildren();

        public Boolean getBoolean(String string, Boolean defaultValue);

        public XmlNode getParent();
        
        public boolean hasParent();
        
        public boolean hasAttribute(String string);

        /** Create a new child and returns it*/
        public XmlNode createChild(String string);

        public void setId(String id);

        public void setTextContent(String text);

        public void appendCDATA(String toString);

        public boolean hasAttributeValue(String metaname, String formdata);
    }

    /**
     * Initializes a new XmlDoc with the given name for the root node
     * @param rootNodeName the name of the root node
     * @return a new XmlDoc
     */
    public static XmlDoc newInstance(String rootNodeName) {
        return new XmlDoc(rootNodeName);
    }

    /**
     * Initializes a new XmlDoc reading the content of the given stream
     * @param inputStream the stream pointing to a xml document
     * @return the requested document. Throws a runtime exception if reading fails
     */
    public static XmlDoc newInstance(InputStream inputStream) {
        return new XmlDoc(inputStream);
    }

    /**
     * Inizializes a new XmlDoc reading the content of the given file
     * @param file the file to read
     * @return the document read from the file or throws a RuntimeException if reading fails.
     */
    public static XmlDoc newInstance(File file) {
        FileInputStream in = null;
        try {
            in = new FileInputStream(file);
            return new XmlDoc(in);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            if(in != null) try {
                in.close();
            } catch (IOException ex) {
                Logger.getLogger(XmlDoc.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * Initializes a new XmlDoc reading the content of the given url
     * @param url the url to read
     * @return the document read from the given url. Throws a RuntimeException if reading fails
     */
    public static XmlDoc newInstance(URL url) {
        InputStream in = null;
        try {
            in = url.openStream();
            return new XmlDoc(in);
        } catch(IOException ex) {
            throw new RuntimeException(ex);
        } finally {
            if(in != null) try {
                in.close();
            } catch (IOException ex) {
                Logger.getLogger(XmlDoc.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /** The real xml document */
    private final Document document;

    /** The real root */
    private final XmlNodeElement root;
    
    public Document getDocument() {
        return document;
    }

    /**
     * Initializes this instance
     * @param rootName the name of the root node
     */
    XmlDoc(String rootName) {
        try {
            document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            root = new XmlNodeElement(document.createElement(rootName));
            document.appendChild(root.element);
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    XmlDoc(Reader in) {
        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            document = builder.parse(new InputSource(in));
            root = new XmlNodeElement(document.getDocumentElement());
        } catch (SAXException ex) {
            throw new RuntimeException(ex);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Read the document from a stream
     * @param in the stream to read. Not closed here.
     */
    XmlDoc(InputStream in) {
        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            document = builder.parse(in);
            root = new XmlNodeElement(document.getDocumentElement());
        } catch (SAXException ex) {
            throw new RuntimeException(ex);
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        } catch (ParserConfigurationException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns the root of this document
     * @return the root of this document
     */
    public XmlNode getRoot() {
        return root;
    }

    /**
     * A private XmlNode
     */
    private class XmlNodeElement implements XmlNode {

        private final Node node;
        private final Element element;

        XmlNodeElement(Node node) {
            this.node = node;
            this.element = node instanceof Element ? (Element) node : null;
        }
        
        public boolean hasAttribute(String name) {
            return element != null && element.hasAttribute(name);
        }
        
        public boolean hasParent() {
            return node.getParentNode() != null;
        }
        
        public XmlNodeElement getParent() {
            return new XmlNodeElement(node.getParentNode());
        }

        public <T extends Enum<T>> T getEnum(Object name, Class<T> type, T defaultValue) {
            if(element == null) return defaultValue;
            if(!element.hasAttribute(String.valueOf(name))) return defaultValue;
            String string = getString(name);
            try {
                return type.cast(type.getMethod("valueOf", String.class).invoke(null, string));
            } catch(Exception ex) {
                return defaultValue;
            }
        }

        public XmlNode setAttribute(Object name, Object value) {
            element.setAttribute(String.valueOf(name), String.valueOf(value));
            return this;
        }

        public XmlNode addChild(Object name) {
            Element childElement = document.createElement(String.valueOf(name));
            node.appendChild(childElement);
            return new XmlNodeElement(element);
        }
        
        public XmlNode createChild(String name) {
            Element childElement = document.createElement(String.valueOf(name));
            node.appendChild(childElement);
            return new XmlNodeElement(childElement);
        }

        public XmlNode setAttributes(Object... nameValuePairs) {
            for (int i = 0; i < nameValuePairs.length; i += 2) {
                String name = (String) nameValuePairs[i];
                Object value = nameValuePairs[i + 1];
                setAttribute(name, value);
            }
            return this;
        }

        public XmlNode setAttribute(Object name, Number value, int decimalDigits) {
            return setAttribute(name, String.format("%." + decimalDigits + "f", value.doubleValue()));
        }

        public String getString(Object name) {
            return element.getAttribute(String.valueOf(name));
        }

        public String getString(Object name, String defaultValue) {
            if(element != null && element.hasAttribute(String.valueOf(name))) {
                return element.getAttribute(String.valueOf(name));
            } else {
                return defaultValue;
            }
        }

        public Double getDouble(Object name) {
            return Double.parseDouble(getString(name));
        }

        public XmlNode getFirstChild(Object childName) {
            final NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                final Node item = children.item(i);
                final String name = item.getNodeName();
                final String childNameString = String.valueOf(childName);
                if (name.equals(childNameString)) {
                    return new XmlNodeElement(item);
                }
            }
            return null;
        }
        
        public XmlNode findFirstChild(Object childName) {
            LinkedList<Node> buffer = new LinkedList<Node>();
            buffer.add(node);
            while(!buffer.isEmpty()) {
                Node current = buffer.pop();
                if(current.getNodeName().equals(String.valueOf(childName))) return new XmlNodeElement(node);
                NodeList childNodes = current.getChildNodes();
                for(int i = 0; i < childNodes.getLength(); i++) {
                    buffer.add(childNodes.item(i));
                }
            }
            return null;
        }

        @Override
        public int hashCode() {
            return element.hashCode();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final XmlNodeElement other = (XmlNodeElement) obj;
            if (this.element != other.element && (this.element == null || !this.element.equals(other.element))) {
                return false;
            }
            return true;
        }

        public Integer getInteger(Object name) {
            String string = getString(name);
            return string == null ? null : Integer.parseInt(string);
        }

        public Iterable<XmlNode> listChildren(Object name) {
            return listChildren(name, false);
        }

        public Iterable<XmlNode> listChildren(Object name, boolean scanChildren) {
            List<XmlNode> scan = listChildren();
            LinkedList<XmlNode> nodes = new LinkedList<XmlNode>();
            for (int i= 0; i < scan.size(); i++) {
                XmlNode child = scan.get(i);
                if(child.getTag().equals(name)) {
                    nodes.add(child);
                }
                if(scanChildren) {
                    scan.addAll(child.listChildren());
                }
            }
            return nodes;
        }

        public List<XmlNode> listChildren() {
            NodeList childNodes = node.getChildNodes();
            ArrayList<XmlNode> children = new ArrayList<XmlNode>(childNodes.getLength());
            for (int i= 0; i < childNodes.getLength(); i++) {
                Node item = childNodes.item(i);
                children.add(new XmlNodeElement(item));
            }
            return children;
        }

        public String getContentAsString() {
            return node.getTextContent();
        }

        public String getTag() {
            return node.getNodeName();
        }

        public Boolean getBoolean(String string, Boolean defaultValue) {
            if(element != null && element.hasAttribute(string)) {
                String att = element.getAttribute(string);
                try {
                    return Boolean.parseBoolean(att);
                } catch(NumberFormatException ex) {
                    return defaultValue;
                }
            } else {
                return defaultValue;
            }
        }

        public void setId(String id) {
            element.setAttribute("id", id);
        }

        public void setTextContent(String text) {
            element.setTextContent(text);
        }
        
        public void appendCDATA(String data) {
            CDATASection sec = getDocument().createCDATASection(data);
        }

        public boolean hasAttributeValue(String name, String value) {
            return element != null && value.equals(element.getAttribute(name));
        }

        public Properties getAttributesAsProperties() {
            Properties p = new Properties();
            NamedNodeMap attributes = element.getAttributes();
            for (int i = 0; i < attributes.getLength(); i++) {
                Node item = attributes.item(i);
                p.setProperty(item.getNodeName(), item.getNodeValue());
            }
            return p;
        }
    }

    /**
     * Returns the content of this XmlDoc as a xml document
     * @return the xml content of this document
     */
    public String toXmlString() {
        try {
            StringWriter buffer = new StringWriter();
            Transformer transformer = TransformerFactory.newInstance().newTransformer();
            DOMSource source = new DOMSource(document);
            StreamResult result = new StreamResult(buffer);
            transformer.transform(source, result);
            return buffer.toString();
        } catch (TransformerException ex) {
            Logger.getLogger(XmlDoc.class.getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }
}
